package org.broadway.jmx;

import java.io.IOException;
import java.util.Date;
import java.util.Map;

import javax.management.AttributeChangeNotification;
import javax.management.MBeanServer;
import javax.management.MBeanServerConnection;
import javax.management.Notification;
import javax.management.NotificationBroadcasterSupport;
import javax.management.NotificationFilterSupport;
import javax.management.NotificationListener;
import javax.management.ObjectName;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.broadway.interfaces.Controllable;
import org.broadway.monitor.BeanMonitor;
import org.broadway.monitor.Monitor;
import org.broadway.monitor.MonitorConstants;
import org.broadway.monitor.MappedResourceCollector;
import org.broadway.monitor.SimpleContext;

/**
 * JmxMonitor is the implementation of a monitor (see {@link #BeanMonitor}).
 * It is a JMX monitor that is designed to observed changes in MBean attribute values.
 * It is desinged to be a completely independent component that provides management
 * and control interface and be deployed as a JMX bean itself.
 * @author vmatters
 * @see JmxMonitorMBean
 * @see BeanMonitor
 * @see Monitor
 * @see Scanner
 */
public class JmxMonitor extends NotificationBroadcasterSupport implements Controllable, BaseJmxBean, JmxMonitorMBean, NotificationListener {

    private static final Log log = LogFactory.getLog(JmxMonitor.class);
    private BeanMonitor monitor;
    private MBeanServerConnection server;
    private ObjectName timerServiceName;
    private boolean created;
    private boolean started;
    private boolean destroyed;
    private Date dateCreated;
    private Date dateStarted;
    private Date dateStopped;
    private Date dateModified;
    private long scanCount;
    private boolean handling;

    /**
     * Constructor that accepts a Monitor object to override internal monitor.
     * @param monitor
     */
    public JmxMonitor(Monitor monitor) {
        setMonitor(monitor);
    }

    /**
     * Constructor that specifies an existing MBeanServer.  If none specified,
     * code will use default.
     * @param server
     */
    public JmxMonitor(MBeanServerConnection server) {
        setMBeanServer(server);
    }

    /**
     * Constructor that takes an MBean server and a monitor.
     * @param server
     * @param monitor
     */
    public JmxMonitor(MBeanServerConnection server, Monitor monitor) {
        setMBeanServer(server);
        setMonitor(monitor);
    }

    /**
     * Setter for MBeanServer
     */
    public void setMBeanServer(MBeanServerConnection server) {
        this.server = server;
    }

    /**
     * Getter for MBeanServer
     * @return
     */
    public MBeanServerConnection getMBeanServer() {
        if (server == null) {
            server = JmxUtil.getMBeanServer();
        }
        return server;
    }

    /**
     * Setter for Monitor.  Setting this will override internal instance.
     */
    public void setMonitor(Monitor monitor) {
        this.monitor = (BeanMonitor) monitor;
    }

    /**
     * Returns a monitor
     * @return
     */
    public Monitor getMonitor() {
        return getInternalMonitor();
    }

    /**
     * Adds an MBean to be observed.  This method will use the default MBeanServer
     * sepcified for the JmxMonitor object.  If you need to specify a different 
     * MBeanServer for the observed object, use addObservedMBean(String,String,MBeanServerConnection)
     * @param alias - A string value that is used to refer the bean in an expression.
     * @param jmxName - the actual MBean's JMX object name value.
     */
    public void addObservedMBean(String alias, String jmxName) {
        getInternalMonitorResCollector().putResource(alias,
                JmxUtil.createObservedJmxBean(this.getMBeanServer(), jmxName));
        log.debug("Added ObservedMBean [alias:" + alias + "][jmxName:" + jmxName + "].");
    }

    /***
     * Adds an MBean to be observed.  This method expects the address of an MBeanServer
     * exposed through the connector service API. 
     * @param alias - A string value that is used to refer to the bean in expressions.
     * @param jmxName - The actual JMX ObjectName reference for the object being observed.
     * @param serverAddress - The fully qualified address of the Server where object lives.
     * Addres must follow the JMX connector format.
     */
    public void addObservedMBean(String alias, String jmxName, String serverAddress) {
        MBeanServerConnection conn = null;
        try {
            conn = JmxUtil.getMBeanServer(serverAddress);
            addObservedMBean(alias, jmxName, conn);
            log.debug("Added ObservedMBean [alias:" + alias + "][jmxName:" + jmxName + "].");
        } catch (JmxException ex) {
            throw new JmxException("Unable to connect to server [" + serverAddress + "]: ", ex);
        }
    }
    
    /***
     * Adds an MBean to be observed.  This method takes a fully-realized MBeanServerConnection instance
     * representing the server where the actual MBean is registered.
     * @param alias - a string value that is used to refer to the bean in expressions.
     * @param jmxName - the actual JMX ObjectName string identifying the object.
     * @param svrConn - an instance of the server connection where object is registered.
     */
    public void addObservedMBean(String alias, String jmxName, MBeanServerConnection svrConn) {
        getInternalMonitorResCollector().putResource(alias,
                JmxUtil.createObservedJmxBean(svrConn, jmxName));
        log.debug("Added ObservedMBean [alias:" + alias + "][jmxName:" + jmxName + "].");
    }
     
     
    /**
     * Adds a bulk set of observed object in the form of a map.
     * @param jmxNames
     * @see addObservedMBean
     */
    public void setObservedMBeans(Map<String, String> jmxNames) {
        if (jmxNames == null) {
            return;
        }

        MappedResourceCollector collector = getInternalMonitorResCollector();
        for (Map.Entry<String, String> e : jmxNames.entrySet()) {
            collector.putResource(e.getKey(),
                    JmxUtil.createObservedJmxBean(this.getMBeanServer(), e.getValue()));
            log.debug("Registered ObservedMBean [" + e.getKey() + "][" + e.getValue() + "].");
        }
    }

    /**
     * Removes the MBean being observed.
     * @param alias the referenced for the MBean (not the ObjectName value).
     * @see addObservedMBean
     */
    public void removeObservedMBean(String alias) {
        if (monitor == null) {
            return;
        }
        getInternalMonitorResCollector().removeResource(alias);
        log.debug("Removed ObservedMBean [" + alias + "]");
    }

    /**
     * Returns the expression used to monitor MBean attribute values.
     * @return String expression
     */
    public String getMonitorExpression() {
        return (monitor != null) ? monitor.getMonitorExpression() : null;
    }

    /**
     * Setter to set the expression used for monitoring.
     * @param exp the expression for the monitor
     */
    public void setMonitorExpression(String exp) {
        this.getInternalMonitor().setMonitorExpression(exp);
    }

    /**
     * Setter for the object name of a JMX Timer service.
     * @param jmxObjectName - object name for timer service
     */
    public void setTimerServiceName(String jmxObjectName) {
        timerServiceName = JmxUtil.createObjectName(jmxObjectName);
        registerForTimerNotifications(timerServiceName);
    }

    /**
     * Getter to return the currently registered JMX Timer service.
     * @return the Jmx ObjectName for registered JMX Timer service.
     */
    public String getTimerServiceName() {
        return (timerServiceName != null) ? timerServiceName.toString() : "UNKNOWN";
    }
    
    /***
     * Flag to indicate when JmxMonitor component will send Action notification 
     * based on the evaluated monitor expression
     * @param flag - true=notification is sent when expression evalutes to true
     * false=notification is sent when expression evaluates to false..
     */
    public void setSendActionNotificationWhen(boolean flag) {
        this.getInternalMonitor().setActWhen(flag);
    }
    
    /***
     * Getter for SendActionNotificationWhen flag.
     * @return returns the flag's value. 
     */
    public boolean getSendActionNotificationWhen() {
        return this.getInternalMonitor().getActWhen();
    }

    
    /**
     * This is the implementation method for NotificationListener.
     * It is called when Monitor receives a timer event.  It causes the monitor
     * to call scan() on its internal Scanner objet.
     *
     * @param notification - notification object
     * @param handback notification handback
     * @see NotificationListener
     */
    public void handleNotification(Notification notification, Object handback) {
        log.debug("Received JMX event notification [" + notification.getType() + "].");
        
        if (!started && handling) {
            return;
        }

        // handle attribute Notification changes
        if (AttributeChangeNotification.class.isAssignableFrom(notification.getClass())) {
            dateModified = new Date();
        }

        // handle timing notification
        if (!handling && notification.getType().equals(JmxUtil.JmxConstants.BROADWAY_TIMER_SIGNAL.toString())) {
            
            log.debug("Handling notification [" + notification.getType() + "].");
          
            try{
                handling = true;
                scanCount++;
                BeanMonitor m = getInternalMonitor();
                
                SimpleContext ctx = (SimpleContext)m.getContext();
                ctx.putValue(MonitorConstants.MonitorKeys.RESOURCE_COLLECTION.toString(), this.getInternalMonitorResCollector().collect());
                m.getEvaluator().putParameter(MonitorConstants.MonitorKeys.CONTEXT.toString(), ctx.getValues());
                
                Boolean result = (Boolean)m.getEvaluator().evaluate(m.getMonitorExpression());
                
                ctx.putValue(MonitorConstants.MonitorKeys.EVALUATOR_RESULT.toString(), result);
                
                log.debug("Monitor expression evaluated to [" + result + "].");
                if(result == m.getActWhen()){
                    this.sendNotification(JmxUtil.createActionNotification(ctx));
                    log.debug("Action notification sent to registered Jmx Action components.");
                }
            }finally{
                handling = false;
            }
        }
    }

    // *************** Lifecycle Methods ******************* //
	/**
	 * Call this lifecycle method after all initial setters and before start().
	 * @throws JmxException
	 */

    public void create() throws JmxException {
        if (created) {
            return;
        }
        if (monitor == null) {
            throw new JmxException("Unable to create component, no internal Monitor object established.");
        }

        created = true;
        dateCreated = new Date();
        log.debug("JmxMonitor object has been created OK on " + dateCreated.toString() + ".");
    }

    /***
	 * Lifecycle - returns the statys of the object's creation.
	 * @return boolean true if object has been created.
	 */
    public boolean isCreated() {
        return created;
    }

    /**
	 * Returns when component was created.
	 * @return String
	 */
    public String getDateCreated() {
        return (dateCreated != null) ? dateCreated.toString() : "UNKNOWN";
    }


    /**
	 * Lifecycle method designated to start the component.
	 * Call this method before executing any encapsulated logic.
	 * If the component is not started, it will not handle timer notification (see {@link #handleNotification(Notification, Object)}).
	 */
    public void start() {
        if (started) {
            return;
        }
        if (!created) {
            create();
        }
        BeanMonitor m = this.getInternalMonitor();
        this.dateStarted = new Date();
        
        SimpleContext ctx = (SimpleContext)m.getContext();
        ctx.putValue(MonitorConstants.MonitorKeys.RESOURCE_COLLECTION.toString(), this.getInternalMonitorResCollector().collect());
        m.getEvaluator().putParameter(MonitorConstants.MonitorKeys.CONTEXT.toString(), ctx.getValues());
        m.getContext().putValue(MonitorConstants.MonitorKeys.STATS_START_TIME, dateStarted);
        
        started = true;
        log.info("JmxMonitor component started OK on " + dateStarted + ".");
    }

    /**
	 * This method reports the current start state
	 * (see {@link #start()})
	 * @return state true if started false otherwise
	 */
    public boolean isStarted() {
        return started;
    }

    /**
	 * Returns last date component started.
	 * @return String
	 */
    public String getDateStarted() {
        return (dateStarted != null) ? dateStarted.toString() : "UNKNOWN";
    }

    /**
	 * This lifecycle method should be called to stop the component while started.
	 * It simply pauses the current state of the component.
	 */
    public void stop() {
        if (!started) {
            return;
        }
        dateStopped = new Date();
        monitor.getContext().putValue(MonitorConstants.MonitorKeys.STATS_STOP_TIME, dateStopped);
        started = false;
        log.info("JmxMonitor component stopped.");
    }

    /**
	 * Returns last date component stopped.
	 * @return String
	 */
    public String getDateStopped() {
        return (dateStopped != null) ? dateStopped.toString() : "UNKNOWN";
    }

    /**
	 * This lifecycle method should be called when the component is being disposed of.
	 * For instance, if the component is running inside a application container, call this
	 * method when the component is being undeployed or the container is shutting down.
	 */
    public void destroy() {
        if (destroyed) {
            return;
        }
        if (started) {
            stop();
        }

        this.scanCount = 0;
        this.dateCreated = null;
        this.dateModified = null;
        this.dateStarted = null;
        this.dateStopped = null;

        destroyed = true;

        log.info("JmxMonitor has been destroyed OK.");
    }

    /***
	 * Lifecycle - returns status of object created state.
	 * @return - true if object has been destroyed.
	 */
    public boolean isDestroyed() {
        return destroyed;
    }


    /**
	 * Returns the last date when component state was modified.
	 * @return String
	 */
    public String getDateModified() {
        return (dateModified != null) ? dateModified.toString() : "UNKNOWN";
    }

    /**
	 * Returns scan count for component.
	 * @return long
	 * @see BeanMonitor
	 * @see Scanner
	 */
    public long getScanCount() {
        return scanCount;
    }

    // ******************** Private Methods ******************** //
	/**
	 * Utility method to handle timer registration.
	 */

    private void registerForTimerNotifications(ObjectName serviceName) throws JmxException {
        log.info("Registering [" + serviceName + "] for Timer service notification.");
        try{
            if (this.getMBeanServer().isRegistered(serviceName)) {
                NotificationFilterSupport filter = new NotificationFilterSupport();
                filter.enableType(JmxUtil.JmxConstants.BROADWAY_TIMER_SIGNAL.toString());
                JmxUtil.registerNotificationListener(this.getMBeanServer(), serviceName,
                        this, filter, null);
            }
        } catch(IOException ex){
            throw new JmxException("Unable to register for timer notifications: ", ex);
        }
    }

    /***
	 * Utility method to return an instance BeanMonitor.
	 * @return
	 */
    private BeanMonitor getInternalMonitor() {
        if (monitor == null) {
            monitor = new BeanMonitor();
            monitor.setAutoAction(false);
            monitor.setActWhen(true);
            monitor.getContext().putValue(MonitorConstants.MonitorKeys.MONITOR_OBJECT.toString(), monitor);
        }
        return monitor;
    }

    /***
	 * Return an instance of the internal registered resources.
	 * @return
	 */
    private MappedResourceCollector getInternalMonitorResCollector() {
        return (MappedResourceCollector) getInternalMonitor().getResourceCollector();
    }
}
